/**
 *  Copyright (c) Filipe Manana <fdmanana@gmail.com>
 *  All rights reserved.
 * 
 *  This file is part of jsdet.
 *
 *  jsdet is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published
 *  by the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  jsdet is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with jsdet.  If not, see <http://www.gnu.org/licenses/>.
 *
 */



package jsdet.parsing.reporting;

import java.util.*;
import java.text.*;
import jsdet.parsing.GlobalParsingStats;
import jsdet.parsing.enums.ClassMemberType;


/**
 * 
 * @author Filipe Manana
 *
 */
public class HtmlReporter extends AbstractReporter {

    private static final String INHERITED_MEMBER_COLOR = "3d71d9";
    private static final String OVERRIDED_MEMBER_COLOR = "0e7931";
    private static final String OWN_MEMBER_COLOR = "000000";

    @Override
    public void reportGlobalParsingStats(GlobalParsingStats stats) {
        classesAlreadyReported.clear();

        doHeader(stats);

        if ( areFunctionsReported() ) {
            reportFunctions(stats);
            System.out.println(emptyRow());
        }

        if ( areClosureObjectsReported() ) {
            reportClosureObjects(stats);
            System.out.println(emptyRow());
        }

        reportClasses(stats);

        doFooter(stats);
    }


    private void reportClasses(GlobalParsingStats stats) {
        Set<String> classes = null;

        switch ( classesSortOrder ) {
            case ASC:
                classes = stats.getClassesOrderedByHierarchyLevelAsc();
                break;

            case DESC:
                classes = stats.getClassesOrderedByHierarchyLevelDesc();
                break;

            case DEFAULT:
            default:
                classes = stats.getClasses();
        }

        System.out.println(startRow());
        System.out.println(cell("<h2>" + createSection("classes", "classes") + "</h2>"));
        System.out.println(emptyCell());
        System.out.println(emptyCell());
        System.out.println(endRow());

        for ( String className : classes ) {

            if ( classesAlreadyReported.contains(className) ) {
                continue;
            }

            if ( !isClassInFilter(className) ) {
                continue;
            }

            if ( classesSortOrder == ClassesSortOrder.DEFAULT ) {
                List<String> upperClasses = stats.getClassBaseHierarchy(className);

                for ( int i = upperClasses.size() - 1; i >= 0; i-- ) {
                    String upperClass = upperClasses.get(i);

                    if ( !stats.hasClass(upperClass) ) {
                        continue;
                    }

                    if ( classesAlreadyReported.contains(upperClass) ) {
                        continue;
                    }

                    if ( !isClassInFilter(upperClass) ) {
                        continue;
                    }

                    reportClass(upperClass, stats);
                    classesAlreadyReported.add(upperClass);
                }
            }

            reportClass(className, stats);
            classesAlreadyReported.add(className);
        }
    }


    private void reportFunctions(GlobalParsingStats stats) {
        Set<String> functions = stats.getFunctions();

        System.out.println(startRow());
        System.out.println(cell("<h2>" + createSection("functions", "functions") + "</h2>", 3));
        System.out.println(endRow());

        for ( String function : functions ) {

            System.out.println(startRow());
            System.out.println(cell("function" + spaces(2) + "<strong>" + function + "</strong>"));
            System.out.println(separatorCell());

            if ( areFileNamesReported() ) {
                String file = stats.getFunctionFile(function);
                System.out.println(cell("file:" + spaces(2) + htmlEscape(file), "noWrap"));
            }
            else {
                System.out.println(emptyCell());
            }

            System.out.println(endRow());
        }
    }


    private void reportClosureObjects(GlobalParsingStats stats) {
        Set<String> closureObjects = stats.getClosureObjects();

        System.out.println(startRow());
        System.out.println(cell("<h2>" + createSection("closure_objects", "closure objects") + "</h2>", 3));
        System.out.println(endRow());

        for ( String closureObject : closureObjects ) {
            Set<String> unknownMembers = stats.getClosureObjectMembers(closureObject, ClassMemberType.NON_FUNCTION);
            Set<String> functionMembers = stats.getClosureObjectMembers(closureObject, ClassMemberType.FUNCTION);

            System.out.println(startRow());
            System.out.println(cell("closure_object" + spaces(2) + "<strong>" + closureObject + "</strong>"));
            System.out.println(separatorCell());

            if ( areFileNamesReported() ) {
                String file = stats.getClosureObjectFile(closureObject);
                System.out.println(cell("file:" + spaces(2) + htmlEscape(file), "noWrap"));
            }
            else {
                System.out.println(emptyCell());
            }

            System.out.println(endRow());

            if ( reportNamesOnly ) {
                continue;
            }

            if ( !unknownMembers.isEmpty() ) {
                System.out.println(startRow());
                System.out.println(cell(spaces(5) + unknownMembers.size() + spaces(1) + ClassMemberType.NON_FUNCTION.getLabel() + spaces(1) + "members"));
                System.out.println(separatorCell());
                System.out.println(emptyCell());
                System.out.println(endRow());
            }

            if ( !functionMembers.isEmpty() ) {
                System.out.println(startRow());
                System.out.println(cell(spaces(5) + functionMembers.size() + spaces(1) + ClassMemberType.FUNCTION.getLabel() + spaces(1) + "members"));
                System.out.println(separatorCell());
                System.out.println(emptyCell());
                System.out.println(endRow());
            }

            System.out.println(emptyRow());

            for ( String member : unknownMembers ) {
                System.out.println(startRow());
                System.out.println(cell(spaces(5) + "<em>" + htmlEscape(member) + "</em>"));
                System.out.println(separatorCell());
                System.out.println(cell("type:" + spaces(1) + ClassMemberType.NON_FUNCTION.getLabel()));
                System.out.println(endRow());
            }

            for ( String member : functionMembers ) {
                System.out.println(startRow());
                System.out.println(cell(spaces(5) + "<em>" + htmlEscape(member) + "</em>"));
                System.out.println(separatorCell());
                System.out.println(cell("type:" + spaces(1) + ClassMemberType.FUNCTION.getLabel()));
                System.out.println(endRow());
            }

            if ( !functionMembers.isEmpty() || !unknownMembers.isEmpty() ) {
                System.out.println(emptyRow());
                System.out.println(emptyRow());
            }
        }
    }


    private void reportClass(String className, GlobalParsingStats stats) {
        reportClassBasicInfo(className, stats);

        if ( reportNamesOnly ) {
            return;
        }

        reportOwnMembersCount(className, stats);
        reportOverridedMembersCount(className, stats);

        if ( areInheritedMembersReported() ) {
            reportInheritedMembersCount(className, stats);
        }

        System.out.println(emptyRow());
        reportNonFunctionMembers(className, stats);
        reportFunctionMembers(className, stats);
        System.out.println(emptyRow());
        System.out.println(emptyRow());
    }


    private void reportInheritedMembersCount(String className, GlobalParsingStats stats) {
        Set<String> members = stats.getClassInheritedMembers(className);
        int inherited = members.size();

        if ( inherited > 0 ) {
            System.out.println(startRow());
            System.out.println(cell(spaces(5) + inherited + spaces(1) + "inherited" + spaces(1) + "members"));
            System.out.println(separatorCell());
            System.out.println(emptyCell());
            System.out.println(endRow());
        }
    }


    private void reportOverridedMembersCount(String className, GlobalParsingStats stats) {
        Set<String> members = stats.getClassOverridingMembers(className);
        int overrided = members.size();

        if ( overrided > 0 ) {
            System.out.println(startRow());
            System.out.println(cell(spaces(5) + overrided + spaces(1) + "overriding" + spaces(1) + "members"));
            System.out.println(separatorCell());
            System.out.println(emptyCell());
            System.out.println(endRow());
        }
    }


    private void reportOwnMembersCount(String className, GlobalParsingStats stats) {
        int owned = stats.getClassUniqueMembers(className).size();

        if ( owned > 0 ) {
            System.out.println(startRow());
            System.out.println(cell(spaces(5) + owned + spaces(1) + "own" + spaces(1) + "members"));
            System.out.println(separatorCell());
            System.out.println(emptyCell());
            System.out.println(endRow());
        }
    }


    private void reportClassBasicInfo(String className, GlobalParsingStats stats) {
        List<String> upperClasses = stats.getClassBaseHierarchy(className);

        StringBuffer line = new StringBuffer("class");
        line.append(spaces(2));
        line.append(className(className));

        for ( String upperClass : upperClasses ) {
            line.append(derives());
            line.append(classReference(upperClass));
        }

        System.out.println(startRow());
        System.out.println(cell(line.toString(), 3));
        System.out.println(endRow());
    }


    private void reportFunctionMembers(String className, GlobalParsingStats stats) {
        reportMembersOfType(className, ClassMemberType.FUNCTION, stats);
    }


    private void reportNonFunctionMembers(String className, GlobalParsingStats stats) {
        reportMembersOfType(className, ClassMemberType.NON_FUNCTION, stats);
    }


    private void reportMembersOfType(String className, ClassMemberType memberType, GlobalParsingStats stats) {

        reportMembers(stats.getClassUniqueMembers(className, memberType), className, memberType.getLabel(), stats);
        reportMembers(stats.getClassOverridingMembers(className, memberType), className, memberType.getLabel(), stats);

        if ( areInheritedMembersReported() ) {
            reportMembers(stats.getClassInheritedMembers(className, memberType), className, memberType.getLabel(), stats);
        }
    }


    private void reportMembers(Set<String> members, String className, String memberTypeLabel,
            GlobalParsingStats stats) {

        for ( String member : members ) {
            String upperClass = stats.findFirstUpperClassWithMember(className, member);
            boolean overriding = stats.isClassMemberOverriding(className, member);
            boolean inherited = stats.classHasInheritedMember(className, member);
            StringBuffer line1 = new StringBuffer("type:");
            StringBuffer line2 = new StringBuffer("");

            line1.append(spaces(1));
            line1.append(spacePad(memberTypeLabel, 10));

            if ( inherited && ( upperClass != null ) ) {
                line2.append("inherited");
                line2.append(spaces(1));
                line2.append("from: ");
                line2.append(classReference(upperClass));
            }
            else if ( overriding && ( upperClass != null ) ) {
                line2.append("overrides:");
                line2.append(spaces(1));
                line2.append(classReference(upperClass));
                line2.append(".");
                line2.append("<em>");
                line2.append(member);
                line2.append("</em>");
            }

            String filesLine = "";
            if ( areFileNamesReported() ) {
                Set<String> files = null;

                if ( inherited ) {
                    files = stats.getFilesWhereClassMemberIsDefined(upperClass, member);
                }
                else {
                    files = stats.getFilesWhereClassMemberIsDefined(className, member);
                }

                for ( Iterator<String> it = files.iterator(); it.hasNext(); ) {
                    String file = it.next();
                    filesLine += "\"" + file + "\"";

                    if ( it.hasNext() ) {
                        filesLine += ", ";
                    }
                }
            }

            String coloredMember = member;

            if ( inherited ) {
                coloredMember = colorize(member, HtmlReporter.INHERITED_MEMBER_COLOR);
            }
            else if ( overriding ) {
                coloredMember = colorize(member, HtmlReporter.OVERRIDED_MEMBER_COLOR);
            }
            else {
                coloredMember = colorize(member, HtmlReporter.OWN_MEMBER_COLOR);
            }

            System.out.print(startRow());
            System.out.print(cell(spaces(5) + "<em>" + coloredMember + "</em>"));
            System.out.print(separatorCell());
            System.out.print(cell(line1.toString()));
            System.out.print(endRow());

            String line2Str = line2.toString();

            if ( !line2Str.isEmpty() ) {
                System.out.print(startRow());
                System.out.print(emptyCell());
                System.out.print(separatorCell());
                System.out.print(cell(line2Str));
                System.out.print(endRow());
            }

            if ( !filesLine.isEmpty() ) {
                System.out.print(startRow());
                System.out.print(emptyCell());
                System.out.print(separatorCell());
                System.out.print(cell("files: " + htmlEscape(filesLine)));
                System.out.print(endRow());
            }

            if ( !line2Str.isEmpty() || !filesLine.isEmpty() ) {
                System.out.print(emptyRow());
            }
        }
    }


    private void doHeader(GlobalParsingStats stats) {
        System.out.println("<html>");
        System.out.println("<head>");
        System.out.println("<style type=\"text/css\">");
        System.out.println(".separator { width: 40px; }");
        System.out.println(".colorSquare { width: 20px; height: 20px; clear: both; float: left; margin-bottom: 5px; }");
        System.out.println(".colorLegend { float: left; margin-left: 10px; }");
        System.out.println(".report td { white-space: wrap; }");
        System.out.println(".report td.noWrap { white-space: nowrap; }");
        System.out.println("</style>");
        System.out.println("</head>");
        System.out.println("<body>");

        SimpleDateFormat dateFormatter = new SimpleDateFormat("E dd MMM yyyy HH:mm:ss");
        System.out.println("jsdet run on" + spaces(3) + dateFormatter.format(new Date()) + newLines(2));

        if ( (originalArgs != null) && (originalArgs.length > 0) ) {
            System.out.print("supplied arguments: " + spaces(3));
            for ( String arg : originalArgs ) {
                System.out.print(arg + spaces(2));
            }
            System.out.println();
            System.out.println(newLines(2));
        }

        if ( areFunctionsReported() ) {
            System.out.print("<strong>" + stats.getFunctions().size() + spaces(1));
            System.out.println(sectionReference("functions", "functions") + "</strong> found.");
            System.out.println(newLines(1));
        }

        if ( areClosureObjectsReported() ) {
            System.out.print("<strong>" + stats.getClosureObjects().size() + spaces(1));
            System.out.println(sectionReference("closure_objects", "closure objects") + "</strong> found.");
            System.out.println(newLines(1));
        }

        System.out.print("<strong>" + stats.getClasses().size() + spaces(1));
        System.out.println(sectionReference("classes", "classes") + "</strong> found.");
        System.out.println(newLines(2));

        doColorInformation();

        System.out.println("<table class=\"report\" border=\"0\">");
    }


    private void doColorInformation() {
        System.out.println("Class members coloring scheme:" + newLines(2));
        System.out.print(colorSquare(HtmlReporter.INHERITED_MEMBER_COLOR));
        System.out.println("<div class=\"colorLegend\"> inherited class members</div>");
        System.out.println(newLines(1));
        System.out.print(colorSquare(HtmlReporter.OVERRIDED_MEMBER_COLOR));
        System.out.println("<div class=\"colorLegend\"> overriding class members</div>");
        System.out.println(newLines(1));
        System.out.print(colorSquare(HtmlReporter.OWN_MEMBER_COLOR));
        System.out.println("<div class=\"colorLegend\"> own class members (not inherited, not overriding)</div>");
        System.out.println("<div style=\"clear: both;\"></div>");
        System.out.println(newLines(3));
    }


    private void doFooter(GlobalParsingStats stats) {
        System.out.println("</table>");
        System.out.println("</body>");
        System.out.println("</html>");
    }


    private String startRow() {
        return "<tr>";
    }


    private String endRow() {
        return "</tr>";
    }


    private String className(String className) {
        return createSection("class_" + className, className);
    }


    private String classReference(String className) {
        return sectionReference("class_" + className, className);
    }


    private String derives() {
        // return spaces(1) + "&lt;" + spaces(1);
        return " &lt;" + spaces(1);
    }


    private String createSection(String sectionName, String sectionText) {
        return "<a name=\"" + sectionName + "\"><strong>" + sectionText + "</strong></a>";
    }


    private String sectionReference(String sectionName, String referenceText) {
        return "<a href=\"#" + sectionName + "\">" + referenceText + "</a>";
    }


    private String separatorCell() {
        return "<td class=\"separator\">&nbsp;</td>";
    }


    private String cell(String content) {
        return cell(content, 0, null);
    }


    private String cell(String content, int colSpan) {
        return cell(content, colSpan, null);
    }


    private String cell(String content, String cssClass) {
        return cell(content, 0, cssClass);
    }


    private String cell(String content, int colSpan, String cssClass) {
        StringBuffer sb = new StringBuffer("<td");

        if ( colSpan > 0 ) {
            sb.append(" colspan=\"");
            sb.append(colSpan);
            sb.append("\"");
        }

        if ( cssClass != null ) {
            sb.append(" class=\"");
            sb.append(cssClass);
            sb.append("\"");
        }

        sb.append(">");
        sb.append(content);
        sb.append("<td>");

        return sb.toString();
    }


    private String emptyCell() {
        return cell(spaces(1));
    }


    private String emptyRow() {
        return "<tr>" + emptyCell() + emptyCell() + emptyCell() + "</tr>";
    }


    private String spaces(int count) {
        StringBuffer sb = new StringBuffer("");

        for ( int i = 0; i < count; i++ ) {
            sb.append("&nbsp;");
        }

        return sb.toString();
    }


    private String newLines(int count) {
        StringBuffer sb = new StringBuffer("");

        for ( int i = 0; i < count; i++ ) {
            sb.append("<br/>");
        }

        return sb.toString();
    }


    private String spacePad(String str, int length) {
        return pad(str, length, spaces(1));
    }


    private String pad(String str, int length, String paddingString) {

        if ( str.length() >= length ) {
            return str;
        }

        StringBuffer sb = new StringBuffer(str);
        int diff = length - str.length();

        for (int i = 0; i < diff; i++ ) {
            sb.append(paddingString);
        }

        return sb.toString();
    }


    private String colorize(String content, String hexColor) {
        return "<span style=\"color: #" + hexColor + "\">" + content + "</span>";
    }


    private String colorSquare(String hexColor) {
        return "<div style=\"background-color: #" + hexColor + "\" class=\"colorSquare\">" + spaces(1) + "</div>";
    }


    private String htmlEscape(String str) {
        str = str.replaceAll("<", "&lt;");
        str = str.replaceAll(">", "&gt;");
        str = str.replaceAll("\"", "&quot;");
        str = str.replaceAll("'", "&#39;");
        str = str.replaceAll("&(?!(?:(?:#\\d+)|\\w+);)", "&amp;");

        return str;
    }
}
